package com.usbserial.core;

import android.os.Handler;
import android.os.Looper;

import com.usbserial.driver.UsbSerialDriver;
import com.usbserial.exception.UsbSerialException;

import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Thread-safe serial port wrapper with background read loop forwarding data to registered listeners.
 */
public class SerialPort implements Closeable {

    private final UsbSerialDriver driver;
    private final SerialConfig config;
    private final CopyOnWriteArrayList<DataListener> listeners = new CopyOnWriteArrayList<>();
    private final AtomicBoolean isOpen = new AtomicBoolean(false);
    private ExecutorService executor;
    private Future<?> readerFuture;
    private final Handler callbackHandler;
    private final android.hardware.usb.UsbDeviceConnection connection;
    private int baudRate;
    private int dataBits;
    private int stopBits;
    private int parity;
    private int flowControl;
    private boolean dtr;
    private boolean rts;

    public SerialPort(android.hardware.usb.UsbDeviceConnection connection,
                      UsbSerialDriver driver,
                      SerialConfig config) {
        this.driver = driver;
        this.config = config;
        this.connection = connection;
        this.callbackHandler = deriveHandler();
        this.baudRate = config.getBaudRate();
        this.dataBits = config.getDataBits();
        this.stopBits = config.getStopBits();
        this.parity = config.getParity();
        this.flowControl = config.getFlowControl();
        this.dtr = false;
        this.rts = false;
    }

    private Handler deriveHandler() {
        try {
            Looper mainLooper = Looper.getMainLooper();
            if (mainLooper != null) {
                return new Handler(mainLooper);
            }
        } catch (RuntimeException ignored) {
            // Running on JVM tests without prepared main looper.
        }
        try {
            Looper current = Looper.myLooper();
            if (current != null) {
                return new Handler(current);
            }
        } catch (RuntimeException ignored) {
            // No looper in this thread.
        }
        return null;
    }

    private void applyConfiguration() throws UsbSerialException {
        driver.setBaudRate(baudRate);
        driver.setDataBits(dataBits);
        driver.setStopBits(stopBits);
        driver.setParity(parity);
        driver.setFlowControl(flowControl);
    }

    private void startReaderLoop() {
        isOpen.set(true);
        try {
            driver.registerReadCallback(new UsbSerialDriver.ReadListener() {
                @Override
                public void onReceived(byte[] data) {
                    if (data == null || data.length == 0) {
                        return;
                    }
                    byte[] payload = new byte[data.length];
                    System.arraycopy(data, 0, payload, 0, data.length);
                    dispatchData(payload);
                }

                @Override
                public void onError(Exception error) {
                    dispatchError(error);
                }
            });
        } catch (UsbSerialException ex) {
            throw new RuntimeException("Failed to register read callback", ex);
        }
    }

    private void stopReaderLoop() {
        try {
            driver.unregisterReadCallback();
        } catch (UsbSerialException ignored) {
        }
        if (readerFuture != null) {
            readerFuture.cancel(true);
        }
        if (executor != null) {
            executor.shutdownNow();
        }
    }

    private void dispatchData(byte[] payload) {
        for (DataListener listener : listeners) {
            postToHandler(() -> listener.onDataReceived(payload));
        }
    }

    private void dispatchError(Exception ex) {
        for (DataListener listener : listeners) {
            postToHandler(() -> listener.onError(ex));
        }
    }

    private void postToHandler(Runnable runnable) {
        if (callbackHandler != null) {
            callbackHandler.post(runnable);
        } else {
            runnable.run();
        }
    }

    public void addDataListener(DataListener listener) {
        if (listener != null) {
            listeners.add(listener);
        }
    }

    public void removeDataListener(DataListener listener) {
        if (listener != null) {
            listeners.remove(listener);
        }
    }

    public synchronized boolean open() throws UsbSerialException {
        if (isOpen()) {
            return true;
        }
        driver.open(connection);
        applyConfiguration();
        startReaderLoop();
        return true;
    }

    public boolean write(byte[] data) throws UsbSerialException {
        if (!isOpen()) {
            throw new UsbSerialException("Port not open");
        }
        driver.write(data, config.getWriteTimeoutMillis());
        return true;
    }

    public boolean isOpen() {
        return isOpen.get();
    }

    public void setDTR(boolean value) throws UsbSerialException {
        driver.setDTR(value);
        this.dtr = value;
    }

    public void setRTS(boolean value) throws UsbSerialException {
        driver.setRTS(value);
        this.rts = value;
    }

    public SerialConfig getConfig() {
        return config;
    }

    public void updateParameters(int baudRate, int dataBits, int stopBits, int parity) throws UsbSerialException {
        driver.setBaudRate(baudRate);
        driver.setDataBits(dataBits);
        driver.setStopBits(stopBits);
        driver.setParity(parity);
        this.baudRate = baudRate;
        this.dataBits = dataBits;
        this.stopBits = stopBits;
        this.parity = parity;
    }

    public void setFlowControl(int flowControl) throws UsbSerialException {
        driver.setFlowControl(flowControl);
        this.flowControl = flowControl;
    }

    @Override
    public void close() throws IOException {
        if (!isOpen.getAndSet(false)) {
            return;
        }
        stopReaderLoop();
        try {
            driver.close();
        } catch (UsbSerialException ex) {
            throw new IOException(ex);
        }
    }

    public List<DataListener> getListeners() {
        return listeners;
    }

    @Override
    public String toString() {
        return "Buad rate : " + baudRate
                + ", Data bits : " + dataBitsToString(dataBits)
                + ", Stop bits : " + stopBitsToString(stopBits)
                + ", Parity : " + parityToString(parity)
                + ", Flow control : " + flowControlToString(flowControl)
                + ", RTS : " + rts
                + ", DTR : " + dtr + " ";
    }

    private String dataBitsToString(int value) {
        switch (value) {
            case SerialConfig.DATABITS_5:
                return "DATABITS_5";
            case SerialConfig.DATABITS_6:
                return "DATABITS_6";
            case SerialConfig.DATABITS_7:
                return "DATABITS_7";
            case SerialConfig.DATABITS_8:
                return "DATABITS_8";
            default:
                return "unknown";
        }
    }

    private String stopBitsToString(int value) {
        switch (value) {
            case SerialConfig.STOPBITS_1:
                return "STOPBITS_1";
            case SerialConfig.STOPBITS_1_5:
                return "STOPBITS_1_5";
            case SerialConfig.STOPBITS_2:
                return "STOPBITS_2";
            default:
                return "unknown";
        }
    }

    private String parityToString(int value) {
        switch (value) {
            case SerialConfig.PARITY_NONE:
                return "PARITY_NONE";
            case SerialConfig.PARITY_ODD:
                return "PARITY_ODD";
            case SerialConfig.PARITY_EVEN:
                return "PARITY_EVEN";
            case SerialConfig.PARITY_MARK:
                return "PARITY_MARK";
            case SerialConfig.PARITY_SPACE:
                return "PARITY_SPACE";
            default:
                return "unknown";
        }
    }

    private String flowControlToString(int value) {
        switch (value) {
            case SerialConfig.FLOW_CONTROL_OFF:
                return "FLOW_CONTROL_OFF";
            case SerialConfig.FLOW_CONTROL_RTS_CTS:
                return "FLOW_CONTROL_RTS_CTS";
            case SerialConfig.FLOW_CONTROL_DSR_DTR:
                return "FLOW_CONTROL_DSR_DTR";
            case SerialConfig.FLOW_CONTROL_XON_XOFF:
                return "FLOW_CONTROL_XON_XOFF";
            default:
                return "unknown";
        }
    }
}
